package cn.springsoter.core.redis.config;

import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import com.google.common.collect.Sets;

import cn.springsoter.core.redis.RedisUtil;
import cn.springsoter.core.redis.constant.MessageConstant;
import cn.springsoter.core.redis.props.RedisProperties;
import cn.springsoter.core.redis.serializer.RedisJsonValueSerializer;
import cn.springsoter.core.redis.serializer.RedisKeySerializer;
import cn.springsoter.core.redis.utils.HelperUtil;
import cn.springsoter.core.redisson.DistributedLocker;
import cn.springsoter.core.redisson.RedissonDistributedLocker;
import cn.springsoter.core.redisson.config.RedissonConfig;
import cn.springsoter.core.redisson.utils.RedissonLockUtil;
import io.lettuce.core.ReadFrom;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisPoolConfig;

/**
 * redis核心配置
 * 
 * @author kevin
 *
 */
@Slf4j
@Configuration
//激活@Cacheable注解
@EnableCaching
@ConditionalOnProperty(name = "spring.redis.host")
public class RedisConfig extends CachingConfigurerSupport{
	
    @Bean
    @ConfigurationProperties(prefix = "spring.redis")
    public RedisProperties redisProperties() {
		return new RedisProperties();
    }
    
    @Bean(destroyMethod = "close")
	public RedissonConfig redissonConfig() {
		return new RedissonConfig(redisProperties());
	}
    
    /**
     * 装配分布式锁，将RedissonClient实例注入到RedissLockUtil中
     *
     * @return
     */
    @Bean
    public DistributedLocker distributedLocker() {
    	RedissonConfig redissonConfig = redissonConfig();
        DistributedLocker locker = new RedissonDistributedLocker();
        ((RedissonDistributedLocker) locker).setRedissonClient(redissonConfig.getSingletonClient());
        RedissonLockUtil.setLocker(locker);
        return locker;
    }
    
    /**
     * redis集群配置
     * 
     * @return	RedisClusterConfiguration
     */
    @Bean
    @ConditionalOnProperty(value = "spring.redis.architecture", havingValue = "cluster")
    public RedisClusterConfiguration redisClusterConfiguration() {
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        Set<HostAndPort> nodes = HelperUtil.getNodes(redisProperties().getHost(), HelperUtil.SPRING_REDIS_HOST_MSG);
    	for(HostAndPort hostAndPort : nodes){
            redisClusterConfiguration.addClusterNode(new RedisNode(hostAndPort.getHost(), hostAndPort.getPort()));
        }
    	if (StringUtils.isNotBlank(redisProperties().getPassword())) {
            redisClusterConfiguration.setPassword(RedisPassword.of(redisProperties().getPassword()));
        }
    	redisClusterConfiguration.setMaxRedirects(redisProperties().getCluster().getMaxRedirects());
    	return redisClusterConfiguration;
    }
    
    /**
     * redis哨兵配置
     * 
     * @return	RedisSentinelConfiguration
     */
    @Bean
    @ConditionalOnProperty(value = "spring.redis.architecture", havingValue = "sentinel")
    public RedisSentinelConfiguration redisSentinelConfiguration(){
        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
        Set<HostAndPort> nodes = HelperUtil.getNodes(redisProperties().getHost(), HelperUtil.SPRING_REDIS_HOST_MSG);
        Set<RedisNode> redisNodes = Sets.newHashSet();
    	for(HostAndPort hostAndPort : nodes){
    		redisNodes.add(new RedisNode(hostAndPort.getHost(), hostAndPort.getPort()));
        }
    	redisSentinelConfiguration.setSentinels(redisNodes);
        if(StringUtils.isNotBlank(redisProperties().getPassword())) {
        	redisSentinelConfiguration.setSentinelPassword(RedisPassword.of(redisProperties().getPassword()));
        	redisSentinelConfiguration.setPassword(RedisPassword.of(redisProperties().getPassword()));
        }
        if(Objects.nonNull(redisProperties().getDatabase())) {
        	redisSentinelConfiguration.setDatabase(redisProperties().getDatabase());
        } 
        redisSentinelConfiguration.setMaster(redisProperties().getSentinel().getMaster());
        return redisSentinelConfiguration;
    }
    
    /**
     * redis主从配置
     * 
     * @return	RedisStaticMasterReplicaConfiguration
     */
    @Bean
    @ConditionalOnProperty(value = "spring.redis.architecture", havingValue = "replication")
    public RedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration(){
    	Set<HostAndPort> masterNode = HelperUtil.getNodes(redisProperties().getMasterHost(), HelperUtil.SPRING_REDIS_MASTER_HOST_MSG);
    	Set<HostAndPort> slaveNodes = HelperUtil.getNodes(redisProperties().getSlavesHost(), HelperUtil.SPRING_REDIS_SLAVE_HOST_MSG);
    	
    	//master只有一个
    	HostAndPort master = masterNode.iterator().next();
    	RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(master.getHost(), master.getPort());
    	slaveNodes.forEach(slave -> staticMasterReplicaConfiguration.addNode(slave.getHost(), slave.getPort()));
        
        if (StringUtils.isNotBlank(redisProperties().getPassword())) {
        	staticMasterReplicaConfiguration.setPassword(RedisPassword.of(redisProperties().getPassword()));
        }
        if(Objects.nonNull(redisProperties().getDatabase())) {
        	staticMasterReplicaConfiguration.setDatabase(redisProperties().getDatabase());
        } 
        return staticMasterReplicaConfiguration;
    }
    
    /**
     * redis单机配置
     * 
     * @return	RedisStandaloneConfiguration
     */
    @Bean
    @ConditionalOnProperty(value = "spring.redis.architecture", havingValue = "standalone")
    public RedisStandaloneConfiguration redisStandaloneConfiguration(){
    	RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
    	Set<HostAndPort> nodes = HelperUtil.getNodes(redisProperties().getHost(), HelperUtil.SPRING_REDIS_HOST_MSG);
    	for(HostAndPort hostAndPort : nodes){
    		redisStandaloneConfiguration.setHostName(hostAndPort.getHost());
    		redisStandaloneConfiguration.setPort(hostAndPort.getPort());
        }
        if (StringUtils.isNotBlank(redisProperties().getPassword())) {
        	redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties().getPassword()));
        }
        if(Objects.nonNull(redisProperties().getDatabase())) {
        	redisStandaloneConfiguration.setDatabase(redisProperties().getDatabase());
        } 
        return redisStandaloneConfiguration;
    }
    
    /**
     * Jedis连接池配置
     * 
     * @return	JedisPoolConfig
     */
    public JedisPoolConfig jedisPoolConfig(){
    	log.info("======= 初始化Jedis连接池配置 ========");
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        //最小空闲连接数
        jedisPoolConfig.setMinIdle(redisProperties().getJedis().getPool().getMinIdle());
        //最大连接数
        jedisPoolConfig.setMaxTotal(redisProperties().getJedis().getPool().getMaxActive());
        //最大空闲连接数
        jedisPoolConfig.setMaxIdle(redisProperties().getJedis().getPool().getMaxIdle());
        //当池内没有可用连接时，最大等待时间
        jedisPoolConfig.setMaxWaitMillis(redisProperties().getJedis().getPool().getMaxWait().toMillis());
        jedisPoolConfig.setTestOnBorrow(false);
        jedisPoolConfig.setTestOnReturn(false);
        jedisPoolConfig.setTestOnCreate(false);
        //其他属性可以自行添加
        return jedisPoolConfig;
    }
    
    /**
     * lettuce连接池配置
     * @return
     */
    private GenericObjectPoolConfig genericObjectPoolConfig() {
    	log.info("======= 初始化lettuce连接池配置 ========");
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxIdle(redisProperties().getJedis().getPool().getMaxIdle());
        genericObjectPoolConfig.setMinIdle(redisProperties().getJedis().getPool().getMinIdle());
        genericObjectPoolConfig.setMaxTotal(redisProperties().getJedis().getPool().getMaxActive());
        genericObjectPoolConfig.setMaxWaitMillis(redisProperties().getJedis().getPool().getMaxWait().toMillis());
        return genericObjectPoolConfig;
    }
    
    /**
     * 连接工厂配置
     * 
     * @return	JedisConnectionFactory
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
    	String mode = redisProperties().getArchitecture();
    	if (StringUtils.isBlank(mode)) {
			throw new RuntimeException(MessageConstant.REDIS_ARCHITECTURE_BLANK_MSG);
		}
		switch (mode) {
		case "sentinel":
			log.info("======= Redis Use Sentinel Mode ========");
			LettuceClientConfiguration sentinelClientConfig = LettucePoolingClientConfiguration.builder()
	                .readFrom(ReadFrom.ANY_REPLICA)
	                .commandTimeout(redisProperties().getTimeout())
	                .poolConfig(genericObjectPoolConfig())
	                .build();
			return new LettuceConnectionFactory(redisSentinelConfiguration(), sentinelClientConfig);
		case "cluster":
			log.info("======= Redis Use Cluster Mode ========");
			//获得默认的连接池构造
	    	JedisClientConfiguration.JedisPoolingClientConfigurationBuilder clusterJedisClientConfig = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder();
	    	//修改我们的连接池配置
	    	clusterJedisClientConfig.poolConfig(jedisPoolConfig());
	    	//通过构造器来构造jedis客户端配置
	    	JedisClientConfiguration clusterJedisClientConfiguration = clusterJedisClientConfig.build();
			return new JedisConnectionFactory(redisClusterConfiguration(), clusterJedisClientConfiguration);
		case "replication":
			log.info("======= Redis Use Replication Mode ========");
			LettuceClientConfiguration replicaClientConfig = LettucePoolingClientConfiguration.builder()
	                .readFrom(ReadFrom.ANY_REPLICA)
	                .commandTimeout(redisProperties().getTimeout())
	                .poolConfig(genericObjectPoolConfig())
	                .build();
			return new LettuceConnectionFactory(redisStaticMasterReplicaConfiguration(), replicaClientConfig);
		case "standalone":
			log.info("======= Redis Use Standalone Mode ========");
			//获得默认的连接池构造
	    	JedisClientConfiguration.JedisPoolingClientConfigurationBuilder standaloneJedisClientConfig = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder();
	    	//修改我们的连接池配置
	    	standaloneJedisClientConfig.poolConfig(jedisPoolConfig());
	    	//通过构造器来构造jedis客户端配置
	    	JedisClientConfiguration standaloneJedisClientConfiguration = standaloneJedisClientConfig.build();
			return new JedisConnectionFactory(redisStandaloneConfiguration(), standaloneJedisClientConfiguration);
		default:
			throw new RuntimeException(MessageConstant.REDIS_ARCHITECTURE_INVALID_VALUE_MSG);
		}
    	
    }

    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        RedisKeySerializer keySerializer = new RedisKeySerializer();
        RedisJsonValueSerializer<Object> valueSerializer = new RedisJsonValueSerializer<>(Object.class);
        //key序列化器
        redisTemplate.setKeySerializer(keySerializer);
        redisTemplate.setHashKeySerializer(keySerializer);
        //value序列化器
		redisTemplate.setValueSerializer(valueSerializer);
		redisTemplate.setHashValueSerializer(valueSerializer);
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        //初始化RedisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisUtil redisUtil() {
    	return new RedisUtil(redisTemplate());
    }
    /**
	 * 使用注解：@Cacheable注解的配置
	 * 
	 * @param redisConnectionFactory
	 * @return
	 */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    	RedisKeySerializer keySerializer = new RedisKeySerializer();
    	RedisJsonValueSerializer<Object> valueSerializer = new RedisJsonValueSerializer<>(Object.class);
        
        // 配置序列化（解决乱码的问题）
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 缓存有效期
                .entryTtl(redisProperties().getTimeout())
                // 禁用空值
//                .disableCachingNullValues()
                // 使用StringRedisSerializer来序列化和反序列化redis的key值
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer))
                // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer));
        //以无锁写入的方式创建RedisCacheWriter
        return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(config)
                .build();
    }
    
    /**
     * 重写KeyGenerator
     * @Cacheable 自定义KeyGenerator，如果不指定KeyGenerator，则使用此配置
     * 
     * 【类全路径.方法名】
     */
    @Override
    @Bean
    public KeyGenerator keyGenerator() {
        return (o, method, objects) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(o.getClass().getName()).append(".");
            sb.append(method.getName()).append(".");
            for (Object obj : objects) {
                sb.append(obj.toString());
            }
            
            String key = sb.toString();
            
            if(StringUtils.lastIndexOf(key, ".") > 1) {
            	key = StringUtils.substringBeforeLast(key, ".");
            }
            return key;
        };
    }
}